/*******************************************************************************
 * This file is part of Crunch Graphics.
 *
 * Crunch Graphics is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Graphics is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Graphics.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.graphics.gpu;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import java.nio.Buffer;
import java.util.Arrays;

/** Encapsulates a texture on the GPU.
 */
public class GpuTexture extends ReferenceCountedGpuObject {
	// notes about OpenGL textures:
	// if mipmapping is enabled in the texture filtering mode, the texture must be mipmap complete
	// the base mipmap level is specified as B
	// if d[i] is a dimension of the ith entry in the mipmap array, then the following must hold:
	//   d[i+1] = floor(d[i] / 2)
	// the mipmap array stops after an entry where each dimension is 1
	// let M = the maximum dimension of the base level
	// this means that a complete mipmap array contains floor(log_2(M)) + 1 entries

	/** Description of 1D data.
	 */
	public static class Data1DDescription {
		public int width;   /**< The width of the data.*/
		public int format;  /**< The format of the data.*/
		public int type;    /**< The type of the data.*/
		public Buffer data; /**< A pointer to the data.*/

		/** Sets default values.
		 */
		public Data1DDescription() {
			width = 0;
			format = GL.GL_RGBA;
			type = GL.GL_UNSIGNED_BYTE;
			data = null;
		}
	}

	/** Description of 2D data.
	 */
	public static class Data2DDescription {
		public int width;   /**< The width of the data.*/
		public int height;  /**< The height of the data, or the number of layers in the array.*/
		public int format;  /**< The format of the data.*/
		public int type;    /**< The type of the data.*/
		public Buffer data; /**< A pointer to the data.*/

		/** Sets default values.
		 */
		public Data2DDescription() {
			width = 0;
			height = 0;
			format = GL.GL_RGBA;
			type = GL.GL_UNSIGNED_BYTE;
			data = null;
		}
	}

	/** Description of 3D data.
	 */
	public static class Data3DDescription {
		public int width;   /**< The width of the data.*/
		public int height;  /**< The height of the data.*/
		public int depth;   /**< The depth of the data, or the number of layers in the array.*/
		public int format;  /**< The format of the data.*/
		public int type;    /**< The type of the data.*/
		public Buffer data; /**< A pointer to the data.*/

		/** Sets default values.
		 */
		public Data3DDescription() {
			width = 0;
			height = 0;
			depth = 0;
			format = GL.GL_RGBA;
			type = GL.GL_UNSIGNED_BYTE;
			data = null;
		}
	}

	/** Description of image settings.
	 */
	public static class ImageDescription {
		public int internalFormat;              /**< The texture's internal format.*/
		public int minFilter;                   /**< The min filter.*/
		public int magFilter;                   /**< The mag filter.*/
		public float[] borderColor;             /**< The border color.*/

		public boolean depthCompareModeEnabled; /**< Whether depth compare mode is enabled.*/
		public int depthCompareFunction;        /**< The depth compare function.*/

		/** Sets default values.
		 */
		public ImageDescription() {
			internalFormat = GL.GL_RGBA8;
			minFilter = GL.GL_LINEAR;
			magFilter = GL.GL_LINEAR;
			borderColor = new float[] { 0.0f, 0.0f, 0.0f, 0.0f };
			depthCompareModeEnabled = false;
			depthCompareFunction = GL.GL_LEQUAL;
		}
	}

	/** Description of mipmap settings.
	 */
	public static class MipmapDescription {
		public int baseLevel;           /**< The base mipmap level.*/
		public int maxLevel;            /**< The maximum mipmap level.*/
		public float minLod;            /**< The minimum LOD level.*/
		public float maxLod;            /**< The maximum LOD level.*/
		public float lodBias;           /**< The LOD bias.*/
		public int levelCount;          /**< The number of mipmaps provided.*/
		public boolean generateMipmaps; /**< Whether mipmaps should be generated.*/

		/** Sets default values.
		 */
		public MipmapDescription() {
			baseLevel = 0;
			maxLevel = 1000;
			minLod = -1000.0f;
			maxLod = 1000.0f;
			lodBias = 0.0f;
			levelCount = 1;
			generateMipmaps = false;
		}
	}

	/** Description of multisample settings.
	 */
	public static class MultisampleDescription {
		public int samples;                     /**< The number of samples per pixel.*/
		public boolean fixedSampleLocations;    /**< Whether sample locations are fixed.*/

		/** Sets default values.
		 */
		public MultisampleDescription() {
			samples = 0;
			fixedSampleLocations = false;
		}
	}

	/** Description used to initialize a 1D texture.
	 */
	public static class Texture1DDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public MipmapDescription mipmapDescription; /**< The mipmap settings.*/
		public Data1DDescription[] mipmapLevels;    /**< The array of mipmap level data.*/
		public int wrapS;                           /**< The S wrapping mode.*/

		/** Sets default values.
		 */
		public Texture1DDescription() {
			imageDescription = new ImageDescription();
			mipmapDescription = new MipmapDescription();
			mipmapLevels = null;
			wrapS = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a 1D array texture.
	 */
	public static class Texture1DArrayDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public MipmapDescription mipmapDescription; /**< The mipmap settings.*/
		public Data2DDescription[] mipmapLevels;    /**< The array of mipmap level data.*/
		public int wrapS;                           /**< The S wrapping mode.*/

		/** Sets default values.
		 */
		public Texture1DArrayDescription() {
			imageDescription = new ImageDescription();
			mipmapDescription = new MipmapDescription();
			mipmapLevels = null;
			wrapS = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a 2D texture.
	 */
	public static class Texture2DDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public MipmapDescription mipmapDescription; /**< The mipmap settings.*/
		public Data2DDescription[] mipmapLevels;    /**< The array of mipmap level data.*/
		public int wrapS;                           /**< The S wrapping mode.*/
		public int wrapT;                           /**< The T wrapping mode.*/

		/** Sets default values.
		 */
		public Texture2DDescription() {
			imageDescription = new ImageDescription();
			mipmapDescription = new MipmapDescription();
			mipmapLevels = null;
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a 2D array texture.
	 */
	public static class Texture2DArrayDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public MipmapDescription mipmapDescription; /**< The mipmap settings.*/
		public Data3DDescription[] mipmapLevels;    /**< The array of mipmap level data.*/
		public int wrapS;                           /**< The S wrapping mode.*/
		public int wrapT;                           /**< The T wrapping mode.*/

		/** Sets default values.
		 */
		public Texture2DArrayDescription() {
			imageDescription = new ImageDescription();
			mipmapDescription = new MipmapDescription();
			mipmapLevels = null;
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a 2D multisample texture.
	 */
	public static class Texture2DMultisampleDescription {
		public ImageDescription imageDescription;               /**< The image settings.*/
		public MultisampleDescription multisampleDescription;   /**< The multisample settings.*/
		public int width;                                       /**< The width of the texture.*/
		public int height;                                      /**< The height of the texture.*/
		public int wrapS;                                       /**< The S wrapping mode.*/
		public int wrapT;                                       /**< The T wrapping mode.*/

		/** Sets default values.
		 */
		public Texture2DMultisampleDescription() {
			imageDescription = new ImageDescription();
			multisampleDescription = new MultisampleDescription();
			width = 0;
			height = 0;
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a 2D multisample array texture.
	 */
	public static class Texture2DMultisampleArrayDescription {
		public ImageDescription imageDescription;               /**< The image settings.*/
		public MultisampleDescription multisampleDescription;   /**< The multisample settings.*/
		public int width;                                       /**< The width of the texture.*/
		public int height;                                      /**< The height of the texture.*/
		public int depth;                                       /**< The number of layers in the array.*/
		public int wrapS;                                       /**< The S wrapping mode.*/
		public int wrapT;                                       /**< The T wrapping mode.*/

		/** Sets default values.
		 */
		public Texture2DMultisampleArrayDescription() {
			imageDescription = new ImageDescription();
			multisampleDescription = new MultisampleDescription();
			width = 0;
			height = 0;
			depth = 0;
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a 2D texture.
	 */
	public static class Texture3DDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public MipmapDescription mipmapDescription; /**< The mipmap settings.*/
		public Data3DDescription[] mipmapLevels;    /**< The array of mipmap level data.*/
		public int wrapS;                           /**< The S wrapping mode.*/
		public int wrapT;                           /**< The T wrapping mode.*/
		public int wrapR;                           /**< The R wrapping mode.*/

		/** Sets default values.
		 */
		public Texture3DDescription() {
			imageDescription = new ImageDescription();
			mipmapDescription = new MipmapDescription();
			mipmapLevels = null;
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
			wrapR = GL.GL_REPEAT;
		}
	}

	/** Description used to initialize a buffer texture.
	 */
	public static class TextureBufferDescription {
		public int internalFormat;  /**< The texture's internal format.*/
		public GpuBuffer buffer;    /**< The underlying buffer.*/

		/** Sets default values.
		 */
		public TextureBufferDescription() {
			internalFormat = GL.GL_RGBA8;
			buffer = null;
		}
	}

	/** Description used to initialize a cube map texture.
	 */
	public static class TextureCubeMapDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public MipmapDescription mipmapDescription; /**< The mipmap settings, which apply to all faces.*/
		public Data2DDescription[][] mipmapLevels;  /**< The array of mipmap level data for each face.*/
		public int wrapS;                           /**< The S wrapping mode.*/
		public int wrapT;                           /**< The T wrapping mode.*/

		/** Sets default values.
		 */
		public TextureCubeMapDescription() {
			imageDescription = new ImageDescription();
			mipmapDescription = new MipmapDescription();
			mipmapLevels = new Data2DDescription[6][];
			wrapS = GL.GL_CLAMP_TO_EDGE;
			wrapT = GL.GL_CLAMP_TO_EDGE;
		}
	}

	/** Description used to initialize a rectangle texture.
	 */
	public static class TextureRectangleDescription {
		public ImageDescription imageDescription;   /**< The image settings.*/
		public Data2DDescription mipmapLevel;       /**< The single mipmap level data.*/
		public int wrapS;                           /**< The S wrapping mode.*/
		public int wrapT;                           /**< The T wrapping mode.*/

		/** Sets default values.
		 */
		public TextureRectangleDescription() {
			imageDescription = new ImageDescription();
			mipmapLevel = new Data2DDescription();
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
		}
	}

	/** Returns the underlying texture ID.
	 */
	public int getId() {
		return id;
	}

	/** Returns the texture's type.
	 */
	public int getType() {
		return type;
	}

	/** Returns the texture's internal format.
	 */
	public int getInternalFormat() {
		return internalFormat;
	}

	/** Sets the image filters.
	 */
	public void setImageFilters(int minFilter, int magFilter) {
		if (minFilter != imageMinFilter) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL.GL_TEXTURE_MIN_FILTER, minFilter);
			imageMinFilter = minFilter;
		}

		if (magFilter != imageMagFilter) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL.GL_TEXTURE_MAG_FILTER, magFilter);
			imageMagFilter = magFilter;
		}
	}

	/** Returns the image min filter.
	 */
	public int getImageMinFilter() {
		return imageMinFilter;
	}

	/** Returns the image mag filter.
	 */
	public int getImageMagFilter() {
		return imageMagFilter;
	}

	/** Sets the image border color.
	 */
	public void setImageBorderColor(float[] borderColor) {
		if (!Arrays.equals(borderColor, imageBorderColor)) {
			bind();
			getGpuState().getGl().glTexParameterfv(type, GL3.GL_TEXTURE_BORDER_COLOR, borderColor, 0);
			System.arraycopy(borderColor, 0, imageBorderColor, 0, 4);
		}
	}

	/** Returns the image border color.
	 */
	public float[] getImageBorderColor() {
		return Arrays.copyOf(imageBorderColor, 4);
	}

	/** Sets whether depth compare mode is enabled.
	 */
	public void setImageDepthCompareModeEnabled(boolean e) {
		if (e != imageDepthCompareModeEnabled) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL3.GL_TEXTURE_COMPARE_MODE, e ? GL3.GL_COMPARE_REF_TO_TEXTURE : GL.GL_NONE);
			imageDepthCompareModeEnabled = e;
		}
	}

	/** Returns whether depth compare mode is enabled.
	 */
	public boolean isImageDepthCompareModeEnabled() {
		return imageDepthCompareModeEnabled;
	}

	/** Sets the depth compare function.
	 */
	public void setImageDepthCompareFunction(int depthCompareFunction) {
		if (imageDepthCompareFunction != depthCompareFunction) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL3.GL_TEXTURE_COMPARE_FUNC, depthCompareFunction);
			imageDepthCompareFunction = depthCompareFunction;
		}
	}

	/** Returns the depth compare function.
	 */
	public int getImageDepthCompareFunction() {
		return imageDepthCompareFunction;
	}

	/** Returns the mipmap level count.
	 */
	public int getMipmapLevelCount() {
		return mipmapLevelCount;
	}

	/** Sets the mipmap levels.
	 */
	public void setMipmapLevels(int baseLevel, int maxLevel) {
		if (baseLevel != mipmapBaseLevel) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL3.GL_TEXTURE_BASE_LEVEL, baseLevel);
			mipmapBaseLevel = baseLevel;
		}

		if (maxLevel != mipmapMaxLevel) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL3.GL_TEXTURE_MAX_LEVEL, maxLevel);
			mipmapMaxLevel = maxLevel;
		}
	}

	/** Returns the mipmap base level.
	 */
	public int getMipmapBaseLevel() {
		return mipmapBaseLevel;
	}

	/** Returns the mipmap max level.
	 */
	public int getMipmapMaxLevel() {
		return mipmapMaxLevel;
	}

	/** Sets the mipmap LOD values.
	 */
	public void setMipmapLod(float minLod, float maxLod, float lodBias) {
		if (minLod != mipmapMinLod) {
			bind();
			getGpuState().getGl().glTexParameterf(type, GL3.GL_TEXTURE_MIN_LOD, minLod);
			mipmapMinLod = minLod;
		}

		if (maxLod != mipmapMaxLod) {
			bind();
			getGpuState().getGl().glTexParameterf(type, GL3.GL_TEXTURE_MAX_LOD, maxLod);
			mipmapMaxLod = maxLod;
		}

		if (lodBias != mipmapLodBias) {
			bind();
			getGpuState().getGl().glTexParameterf(type, GL3.GL_TEXTURE_LOD_BIAS, lodBias);
			mipmapLodBias = lodBias;
		}
	}

	/** Returns the mipmap minimum LOD.
	 */
	public float getMipmapMinLod() {
		return mipmapMinLod;
	}

	/** Returns the mipmap maximum LOD.
	 */
	public float getMipmapMaxLod() {
		return mipmapMaxLod;
	}

	/** Returns the mipmap LOD bias.
	 */
	public float getMipmapLodBias() {
		return mipmapLodBias;
	}

	/** Returns the number of multisample samples.
	 */
	public int getMultisampleSamples() {
		return multisampleSamples;
	}

	/** Returns whether multisample sample locations are fixed.
	 */
	public boolean areMultisampleSampleLocationsFixed() {
		return multisampleFixedSampleLocations;
	}

	/** Returns the texture size in the given dimension, or the number of layers in the array.
	 */
	public int getSize(int dimension) {
		return size[dimension];
	}

	/** Returns the texture width.
	 */
	public int getWidth() {
		return size[0];
	}

	/** Returns the texture height, or the number of layers in the array.
	 */
	public int getHeight() {
		return size[1];
	}

	/** Returns the texture depth, or the number of layers in the array.
	 */
	public int getDepth() {
		return size[2];
	}

	/** Sets the S wrap mode.
	 */
	public void setWrapS(int s) {
		if (s != wrapS) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL.GL_TEXTURE_WRAP_S, s);
			wrapS = s;
		}
	}

	/** Returns the S wrap mode.
	 */
	public int getWrapS() {
		return wrapS;
	}

	/** Sets the T wrap mode.
	 */
	public void setWrapT(int t) {
		if (t != wrapT) {
			bind();
			getGpuState().getGl().glTexParameteri(type, GL.GL_TEXTURE_WRAP_T, t);
			wrapT = t;
		}
	}

	/** Returns the T wrap mode.
	 */
	public int getWrapT() {
		return wrapT;
	}

	/** Sets the R wrap mode.
	 */
	public void setWrapR(int r) {
		if (r != wrapR) {
			getGpuState().getGl().glTexParameteri(type, GL3.GL_TEXTURE_WRAP_R, r);
			wrapR = r;
		}
	}

	/** Returns the R wrap mode.
	 */
	public int getWrapR() {
		return wrapR;
	}

	/** Returns the underlying buffer for a buffer texture.\
	 */
	public GpuBuffer getBuffer() {
		return buffer;
	}

	/** Binds the texture to the currently active texture image unit.
	 */
	public void bind() {
		getGpuState().getActiveTextureImageUnit().bindTexture(this);
	}

	/** Binds the texture to the given texture image unit.
	 */
	public void bind(int textureImageUnit) {
		getGpuState().getTextureImageUnit(textureImageUnit).bindTexture(this);
	}

	GpuTexture(GpuState state, Texture1DDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = GL.GL_REPEAT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.mipmapLevels[0].width;
		size[1] = 0;
		size[2] = 0;
		init(GL3.GL_TEXTURE_1D, desc.imageDescription, desc.mipmapDescription, null, true);
		for (int i = 0; i < desc.mipmapDescription.levelCount; ++i) {
			getGpuState().getGl().glTexImage1D(
					GL3.GL_TEXTURE_1D,
					desc.mipmapDescription.baseLevel + i,
					desc.imageDescription.internalFormat,
					desc.mipmapLevels[i].width,
					0,
					desc.mipmapLevels[i].format,
					desc.mipmapLevels[i].type,
					desc.mipmapLevels[i].data);
		}

		if (desc.mipmapDescription.generateMipmaps) {
			getGpuState().getGl().glGenerateMipmap(type);
		}
	}

	GpuTexture(GpuState state, Texture1DArrayDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = GL.GL_REPEAT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.mipmapLevels[0].width;
		size[1] = desc.mipmapLevels[0].height;
		size[2] = 0;
		init(GL3.GL_TEXTURE_1D_ARRAY, desc.imageDescription, desc.mipmapDescription, null, true);
		for (int i = 0; i < desc.mipmapDescription.levelCount; ++i) {
			getGpuState().getGl().glTexImage2D(
					GL3.GL_TEXTURE_1D_ARRAY,
					desc.mipmapDescription.baseLevel + i,
					desc.imageDescription.internalFormat,
					desc.mipmapLevels[i].width,
					desc.mipmapLevels[i].height,
					0,
					desc.mipmapLevels[i].format,
					desc.mipmapLevels[i].type,
					desc.mipmapLevels[i].data);
		}

		if (desc.mipmapDescription.generateMipmaps) {
			getGpuState().getGl().glGenerateMipmap(type);
		}
	}

	GpuTexture(GpuState state, Texture2DDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.mipmapLevels[0].width;
		size[1] = desc.mipmapLevels[0].height;
		size[2] = 0;
		init(GL.GL_TEXTURE_2D, desc.imageDescription, desc.mipmapDescription, null, true);
		for (int i = 0; i < desc.mipmapDescription.levelCount; ++i) {
			getGpuState().getGl().glTexImage2D(
					GL.GL_TEXTURE_2D,
					desc.mipmapDescription.baseLevel + i,
					desc.imageDescription.internalFormat,
					desc.mipmapLevels[i].width,
					desc.mipmapLevels[i].height,
					0,
					desc.mipmapLevels[i].format,
					desc.mipmapLevels[i].type,
					desc.mipmapLevels[i].data);
		}

		if (desc.mipmapDescription.generateMipmaps) {
			getGpuState().getGl().glGenerateMipmap(type);
		}
	}

	GpuTexture(GpuState state, Texture2DArrayDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.mipmapLevels[0].width;
		size[1] = desc.mipmapLevels[0].height;
		size[2] = desc.mipmapLevels[0].depth;
		init(GL.GL_TEXTURE_2D_ARRAY, desc.imageDescription, desc.mipmapDescription, null, true);
		for (int i = 0; i < desc.mipmapDescription.levelCount; ++i) {
			getGpuState().getGl().glTexImage3D(
					GL3.GL_TEXTURE_2D_ARRAY,
					desc.mipmapDescription.baseLevel + i,
					desc.imageDescription.internalFormat,
					desc.mipmapLevels[i].width,
					desc.mipmapLevels[i].height,
					desc.mipmapLevels[i].depth,
					0,
					desc.mipmapLevels[i].format,
					desc.mipmapLevels[i].type,
					desc.mipmapLevels[i].data);
		}

		if (desc.mipmapDescription.generateMipmaps) {
			getGpuState().getGl().glGenerateMipmap(type);
		}
	}

	GpuTexture(GpuState state, Texture2DMultisampleDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.width;
		size[1] = desc.height;
		size[2] = 0;
		init(GL3.GL_TEXTURE_2D_MULTISAMPLE, desc.imageDescription, null, desc.multisampleDescription, true);
		getGpuState().getGl().glTexImage2DMultisample(
				GL3.GL_TEXTURE_2D_MULTISAMPLE,
				desc.multisampleDescription.samples,
				desc.imageDescription.internalFormat,
				desc.width,
				desc.height,
				desc.multisampleDescription.fixedSampleLocations);
	}

	GpuTexture(GpuState state, Texture2DMultisampleArrayDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.width;
		size[1] = desc.height;
		size[2] = desc.depth;
		init(GL3.GL_TEXTURE_2D_MULTISAMPLE_ARRAY, desc.imageDescription, null, desc.multisampleDescription, true);
		getGpuState().getGl().glTexImage3DMultisample(
				GL3.GL_TEXTURE_2D_MULTISAMPLE_ARRAY,
				desc.multisampleDescription.samples,
				desc.imageDescription.internalFormat,
				desc.width,
				desc.height,
				desc.depth,
				desc.multisampleDescription.fixedSampleLocations);
	}

	GpuTexture(GpuState state, Texture3DDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = desc.wrapR;
		size[0] = desc.mipmapLevels[0].width;
		size[1] = desc.mipmapLevels[0].height;
		size[2] = desc.mipmapLevels[0].depth;
		init(GL3.GL_TEXTURE_3D, desc.imageDescription, desc.mipmapDescription, null, true);
		for (int i = 0; i < desc.mipmapDescription.levelCount; ++i) {
			getGpuState().getGl().glTexImage3D(
					GL3.GL_TEXTURE_3D,
					desc.mipmapDescription.baseLevel + i,
					desc.imageDescription.internalFormat,
					desc.mipmapLevels[i].width,
					desc.mipmapLevels[i].height,
					desc.mipmapLevels[i].depth,
					0,
					desc.mipmapLevels[i].format,
					desc.mipmapLevels[i].type,
					desc.mipmapLevels[i].data);
		}

		if (desc.mipmapDescription.generateMipmaps) {
			getGpuState().getGl().glGenerateMipmap(type);
		}
	}

	GpuTexture(GpuState state, TextureBufferDescription desc) {
		super(state);
		size[0] = (int)desc.buffer.getSize();
		size[1] = 0;
		size[2] = 0;
		buffer = desc.buffer;
		buffer.acquire();
		init(GL3.GL_TEXTURE_BUFFER, null, null, null, false);
	}

	GpuTexture(GpuState state, TextureCubeMapDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.mipmapLevels[0][0].width;
		size[1] = desc.mipmapLevels[0][0].height;
		size[2] = 0;
		init(GL.GL_TEXTURE_CUBE_MAP, desc.imageDescription, desc.mipmapDescription, null, true);
		for (int f = 0; f < 6; ++f) {
			for (int i = 0; i < desc.mipmapDescription.levelCount; ++i) {
				getGpuState().getGl().glTexImage2D(
						GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X + f,
						desc.mipmapDescription.baseLevel + i,
						desc.imageDescription.internalFormat,
						desc.mipmapLevels[f][i].width,
						desc.mipmapLevels[f][i].height,
						0,
						desc.mipmapLevels[f][i].format,
						desc.mipmapLevels[f][i].type,
						desc.mipmapLevels[f][i].data);
			}
		}

		if (desc.mipmapDescription.generateMipmaps) {
			getGpuState().getGl().glGenerateMipmap(type);
		}
	}

	GpuTexture(GpuState state, TextureRectangleDescription desc) {
		super(state);
		wrapS = desc.wrapS;
		wrapT = desc.wrapT;
		wrapR = GL.GL_REPEAT;
		size[0] = desc.mipmapLevel.width;
		size[1] = desc.mipmapLevel.height;
		size[2] = 0;
		init(GL3.GL_TEXTURE_RECTANGLE, desc.imageDescription, null, null, true);
		getGpuState().getGl().glTexImage2D(
				GL3.GL_TEXTURE_RECTANGLE,
				0,
				desc.imageDescription.internalFormat,
				desc.mipmapLevel.width,
				desc.mipmapLevel.height,
				0,
				desc.mipmapLevel.format,
				desc.mipmapLevel.type,
				desc.mipmapLevel.data);
	}

	protected void onRelease() {
		getGpuState().getGl().glDeleteTextures(1, new int[] { id }, 0);
		if (buffer != null) {
			buffer.release();
			buffer = null;
		}
	}

	// initializes values
	private void init(
			int textureType,
			ImageDescription imageDesc,
			MipmapDescription mipmapDesc,
			MultisampleDescription multisampleDesc,
			boolean setWrap) {
		// generate a texture and bind it on the current texture unit
		int[] buf = new int[1];
		getGpuState().getGl().glGenTextures(1, buf, 0);
		id = buf[0];
		type = textureType;
		bind();

		// assign default values for missing descriptions

		if (imageDesc == null) {
			internalFormat = GL.GL_RGBA8;
			imageMinFilter = GL.GL_LINEAR;
			imageMagFilter = GL.GL_LINEAR;
			imageDepthCompareModeEnabled = false;
			imageDepthCompareFunction = GL.GL_LEQUAL;
		} else {
			internalFormat = imageDesc.internalFormat;

			getGpuState().getGl().glTexParameteri(textureType, GL.GL_TEXTURE_MIN_FILTER, imageDesc.minFilter);
			imageMinFilter = imageDesc.minFilter;

			getGpuState().getGl().glTexParameteri(textureType, GL.GL_TEXTURE_MAG_FILTER, imageDesc.magFilter);
			imageMagFilter = imageDesc.magFilter;

			getGpuState().getGl().glTexParameteri(textureType, GL3.GL_TEXTURE_COMPARE_MODE, imageDesc.depthCompareModeEnabled ? GL3.GL_COMPARE_REF_TO_TEXTURE : GL.GL_NONE);
			imageDepthCompareModeEnabled = imageDesc.depthCompareModeEnabled;

			getGpuState().getGl().glTexParameteri(textureType, GL3.GL_TEXTURE_COMPARE_FUNC, imageDesc.depthCompareFunction);
			imageDepthCompareFunction = imageDesc.depthCompareFunction;
		}

		if (mipmapDesc == null) {
			mipmapLevelCount = 0;
			mipmapBaseLevel = 0;
			mipmapMaxLevel = 1000;
			mipmapMinLod = -1000.0f;
			mipmapMaxLod = 1000.0f;
			mipmapLodBias = 0.0f;
		} else {
			mipmapLevelCount = mipmapDesc.levelCount;

			getGpuState().getGl().glTexParameteri(textureType, GL3.GL_TEXTURE_BASE_LEVEL, mipmapDesc.baseLevel);
			mipmapBaseLevel = mipmapDesc.baseLevel;

			getGpuState().getGl().glTexParameteri(textureType, GL3.GL_TEXTURE_MAX_LEVEL, mipmapDesc.maxLevel);
			mipmapMaxLevel = mipmapDesc.maxLevel;

			getGpuState().getGl().glTexParameterf(textureType, GL3.GL_TEXTURE_MIN_LOD, mipmapDesc.minLod);
			mipmapMinLod = mipmapDesc.minLod;

			getGpuState().getGl().glTexParameterf(textureType, GL3.GL_TEXTURE_MAX_LOD, mipmapDesc.maxLod);
			mipmapMaxLod = mipmapDesc.maxLod;

			getGpuState().getGl().glTexParameterf(textureType, GL3.GL_TEXTURE_LOD_BIAS, mipmapDesc.lodBias);
			mipmapLodBias = mipmapDesc.lodBias;
		}

		if (multisampleDesc == null) {
			multisampleSamples = 0;
			multisampleFixedSampleLocations = false;
		} else {
			multisampleSamples = multisampleDesc.samples;
			multisampleFixedSampleLocations = multisampleDesc.fixedSampleLocations;
		}

		// these should be set by the calling function
		if (setWrap) {
			getGpuState().getGl().glTexParameteri(textureType, GL.GL_TEXTURE_WRAP_S, wrapS);
			getGpuState().getGl().glTexParameteri(textureType, GL.GL_TEXTURE_WRAP_T, wrapT);
			getGpuState().getGl().glTexParameteri(textureType, GL3.GL_TEXTURE_WRAP_R, wrapR);
		} else {
			wrapS = GL.GL_REPEAT;
			wrapT = GL.GL_REPEAT;
			wrapR = GL.GL_REPEAT;
		}
	}

	private int id;             // texture ID
	private int type;           // texture type
	private int internalFormat; // internal texture format

	private int imageMinFilter;                         // the image min filter
	private int imageMagFilter;                         // the image mag filter
	private float[] imageBorderColor = new float[4];    // the image border color

	private boolean imageDepthCompareModeEnabled;   // whether depth stencil compare mode is enabled
	private int imageDepthCompareFunction;          // the image depth compare function

	private int mipmapLevelCount;   // number of mipmap levels
	private int mipmapBaseLevel;    // mipmap base level
	private int mipmapMaxLevel;     // mipmap max level
	private float mipmapMinLod;     // mipmap min LOD
	private float mipmapMaxLod;     // mipmap max LOD
	private float mipmapLodBias;    // mipmap LOD bias

	private int multisampleSamples;                     // number of multisample samples
	private boolean multisampleFixedSampleLocations;    // whether multisample sample locations are fixed

	private int[] size = new int[3];    // texture size in each dimension, or number of array layers

	private int wrapS;  // S wrap mode
	private int wrapT;  // T wrap mode
	private int wrapR;  // R wrap mode

	private GpuBuffer buffer;   // underlying buffer for buffer textures
}